﻿using System;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Caius.Core;
using Caius.Core.Commands;
using Caius.Core.Events;
using Caius.Utils.Helpers;

namespace Caius
{
    [Guid("7E99625A-4735-44AD-BA42-62BCFE9C8285")]
    [ProgId("AddIn.AsteriskManager")]    
    [ClassInterface(ClassInterfaceType.AutoDispatch)]
    public class AsteriskManager : IInitDone
    {
        private Thread listenReceiveThread;
        private bool isMonitoring = false;
        private volatile bool stopReceiveThread = false;
        private volatile string monitoringChannel = "";
        private volatile string tempCallerId = "";
        private volatile bool pauseMonitor = false;
        private bool isConnected = false;
        private volatile string myId = "";


        private delegate void ListenForReceive();
        
        /// <summary>
        /// Конструктор по-умолчанию
        /// </summary>
        public AsteriskManager() 
        {            
        }

        /// <summary>
        /// Инициализация внешней компоненты
        /// </summary>
        /// <param name="pConnection"></param>
        public void Init([MarshalAs(UnmanagedType.IDispatch)] Object pConnection)
        {
            try
            {
                ExternalAddIn.App = pConnection;
                ExternalAddIn.ExtEvent = (IAsyncEvent)pConnection;
            }
            catch
            {
                throw new COMException(@"Unknown object context");
            }
        }

        /// <summary>
        /// Освобождение ресурсов при уничтожении экземпляра компоненты в 1С
        /// </summary>
        public void Done()
        {
            ExternalAddIn.App = null;
            ExternalAddIn.ExtEvent = null;
        }

        /// <summary>
        /// Информация о модуле
        /// </summary>
        /// <param name="pInfo"></param>
        public void GetInfo([MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_VARIANT)] ref Object[] pInfo)
        {
            pInfo[0] = @"2000"; 
        }

        /// <summary>
        /// Тест генерации внешнего события
        /// </summary>
        /// <param name="data">Данные</param>
        public void TestExtEvent([MarshalAs(UnmanagedType.BStr)] String data) 
        {
            EventGenerator.GenerateExtEvent(@"TestEvent", data);
        }   
        
        /// <summary>
        /// Открывает подключение к Asterisk Manager. 
        /// </summary>
        /// <param name="host">Cтрока с именем хоста. Допускается указание либо имени либо ip адреса. Обязателен. </param>
        /// <param name="port">Номер порта на котором висит Manager. Не обязателен. По умолчанию 5038 </param>
        /// <param name="username">Строка с именем пользователя Asterisk Manager. Обязателен. </param>
        /// <param name="password">Строка с паролем пользователя Asterisk Manager. Обязателен. </param>
        /// <param name="result">Возвращает true в случае успешного подключения, либо false. </param>
        public bool Connect([MarshalAs(UnmanagedType.BStr)] String host, [MarshalAs(UnmanagedType.SysUInt)] Int32 port,
                            [MarshalAs(UnmanagedType.BStr)] String username, [MarshalAs(UnmanagedType.BStr)] String password, 
                            [MarshalAs(UnmanagedType.Bool)] ref bool result)
        {
            if (isConnected)
                throw new COMException("You already connected.");

            if (!Regex.Match(host, "\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b").Success)
                host = Dns.GetHostAddresses(host)[0].ToString();

            // Соединение с сервером         
            IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse(host), port);
            ApplicationManager.Instance.InitSocket();
            try
            {
                ApplicationManager.Instance.ClientSocket.Connect(serverEndPoint);
            }
            catch (ArgumentNullException anex) { throw new COMException(anex.Message); }
            catch (SocketException socex) { throw new COMException(socex.Message); }
            catch (ObjectDisposedException odex) { throw new COMException(odex.Message); }
            catch (SecurityException secex) { throw new COMException(secex.Message); }
            catch (InvalidOperationException ioex) { throw new COMException(ioex.Message); }
            catch (Exception ex)
            {
                //Вывод подробной информации об исключении
                //с вложенностью 2 уровня внутренних исключений

                StringBuilder message = new StringBuilder(1000);
                StringBuilder stack = new StringBuilder(1000);
                StringBuilder exceptionData = new StringBuilder(2000);

                message.AppendFormat("Message: {0}\r\n", ex.Message);
                stack.AppendFormat("Stack trace: {0}\r\n", ex.StackTrace);
                Exception innerEx1 = ex.InnerException;
                if (innerEx1 != null)
                {
                    message.AppendFormat("Inner Message 1: {0}\r\n", innerEx1.Message);
                    stack.AppendFormat("Inner Stack trace 1: {0}\r\n", innerEx1.StackTrace);

                    Exception innerEx2 = innerEx1.InnerException;
                    if (innerEx2 != null)
                    {
                        message.AppendFormat("Inner Message 2: {0}\r\n", innerEx2.Message);
                        stack.AppendFormat("Inner Stack trace 2: {0}\r\n", innerEx2.StackTrace);

                    }
                }

                exceptionData.AppendLine(message.ToString());
                exceptionData.AppendLine(stack.ToString());

                throw new COMException(exceptionData.ToString());
            }

            isConnected = true;

            // Авторизация
            StringBuilder cmdBuilder = new StringBuilder();
            cmdBuilder.Append("Action: Login\r\n");
            cmdBuilder.AppendFormat("Username: {0}\r\n", username);
            cmdBuilder.AppendFormat("Secret: {0}\r\n", password);
            cmdBuilder.Append("ActionID: 1\r\n");
            cmdBuilder.Append("\r\n");

            foreach (string elem in CommandDispatcher.Exec(cmdBuilder.ToString()).ResultData)
            {
                if (Regex.Match(elem, "Message: Authentication accepted", RegexOptions.IgnoreCase).Success)
                {
                    myId = username;
                    result = isConnected;
                    return isConnected;
                }
                else if (Regex.Match(elem, "Message: Authentication failed", RegexOptions.IgnoreCase).Success)
                {
                    result = false;
                    Disconnect();
                    throw new COMException("Authentication failed. Disconnecting...");
                }
            }
            return false;            
        }

        /// <summary>
        /// Запускает мониторинг за заданным каналом
        /// </summary>
        /// <param name="channel">Строка с именем канала. Обязателен.</param>
        public void Ready([MarshalAs(UnmanagedType.BStr)] String channel)
        {
            if (!isConnected)
                throw new COMException("You are not connected.");

            if (isMonitoring)
                throw new COMException("You must to stop current monitoring before starting a new.");


            //monitoringChannel = channel;
            //stopReceiveThread = false;

            //int bytesRead = 0;
            //byte[] buffer;
            //do
            //{
            //    buffer = new byte[1024];
            //    try
            //    {
            //        bytesRead = ApplicationManager.Instance.ClientSocket.Receive(buffer);
            //    }
            //    catch (SocketException socex)
            //    {
            //        break;
            //    }
            //} while (bytesRead > 0);

            //Запуск потока, в реальном времени прослушивающего сокет клиента
            ListenForReceive lfr = this.StartListenForReceive;
            listenReceiveThread = new Thread(new ThreadStart(lfr));
            listenReceiveThread.Start();
        }

        /// <summary>
        /// Инициирует исходящий вызов посредством команды AMI originate.
        /// </summary>
        /// <param name="channel">Строка с названием канала, с которого совершается исходящий вызов. Обязателен.</param>
        /// <param name="context">Строка с названием контекста для совершения исходящего вызова. Обязателен.</param>
        /// <param name="exten">Строка с именем абонента. Обязателен.</param>
        /// <param name="priority">Строка с приоритетом, с которого должен выполнятся вызов в заданном контексте. Обязателен.</param>
        /// <param name="timeout">Строка с таймаутом (в миллисекундах) для соединения с инициатором исходящего вызова.  По умолчанию 30000. Не обязателен.</param>
        /// <param name="callerid">Строка со значением CallerID,  используемым для совершения исходящего вызова. Не обязателен.</param>
        /// <param name="async">Строка со значением использования асинхронного вызова, true или false. Не обязателен.</param>
        /// <param name="actionid">Строка с идентификатором  запроса. Позволяет сопоставить результат выполнения конкретному запросу. Не обязателен.</param>
        public void Dial([MarshalAs(UnmanagedType.BStr)] String channel, [MarshalAs(UnmanagedType.BStr)] String context,
                        [MarshalAs(UnmanagedType.BStr)] String exten, [MarshalAs(UnmanagedType.BStr)] String priority,
                        [MarshalAs(UnmanagedType.BStr)] String timeout, [MarshalAs(UnmanagedType.BStr)] String callerId,
                        [MarshalAs(UnmanagedType.BStr)] String async, [MarshalAs(UnmanagedType.BStr)] String actionId)
        {
            if (!isConnected)
                throw new COMException("You are not connected.");

            if (isMonitoring)
            {
                isMonitoring = false;
                stopReceiveThread = true;
                pauseMonitor = true;
            }
            
            StringBuilder cmdBuilder = new StringBuilder();
            cmdBuilder.AppendFormat("Action: Originate\r\n");
            cmdBuilder.AppendFormat("Channel: {0}\r\n", channel);
            cmdBuilder.AppendFormat("Context: {0}\r\n", context);
            cmdBuilder.AppendFormat("Exten: {0}\r\n", exten);
            cmdBuilder.AppendFormat("Priority: {0}\r\n", priority);
            cmdBuilder.AppendFormat("Timeout: {0}\r\n", timeout);
            cmdBuilder.AppendFormat("CallerID: {0}\r\n", callerId);
            cmdBuilder.AppendFormat("Async: {0}\r\n", async);
            cmdBuilder.AppendFormat("ActionID: {0}\r\n", actionId);
            cmdBuilder.AppendFormat("\r\n");

            monitoringChannel = channel;
            //tempCallerId = channel.Split('/')[1].Trim().Substring(0, 4);
            
            CommandDispatcher.Exec(cmdBuilder.ToString());

            if (pauseMonitor)
            {
                pauseMonitor = false;
                stopReceiveThread = false;
                Thread.Sleep(1000);
                Ready(monitoringChannel);                
            }
        }

        /// <summary>
        /// Останавливает мониторинг за каналом. 
        /// </summary>
        public void Stop([MarshalAs(UnmanagedType.BStr)] String channel)
        {
            if (!isConnected)
                throw new COMException("You are not connected.");
            stopReceiveThread = true;
            Thread.Sleep(1000);
            if (!pauseMonitor)
                monitoringChannel = "";

        }

        /// <summary>
        /// Закрывает соединение с сервером.
        /// </summary>
        public void Disconnect()
        {
            if (!isConnected)
                throw new COMException("You are not connected.");

            isConnected = false;
            stopReceiveThread = true;            
            Thread.Sleep(1000);
            isMonitoring = false;
            monitoringChannel = "";

            CommandDispatcher.Exec("ACTION: Logoff\r\n\r\n");
            Thread.Sleep(1000);
            ApplicationManager.Instance.ClientSocket.Disconnect(false);
            Thread.Sleep(1000);
            ApplicationManager.Instance.ClientSocket.Close(10);
        }

        /// <summary>
        /// Возвращает CallerID последнего позвонившего абонента.
        /// </summary>
        /// <param name="callerId">CallerID позвонившего абонента.</param>
        public void GetCID([MarshalAs(UnmanagedType.BStr)] ref String callerId)
        {
            if (!isConnected)
                callerId = "";
            else
                callerId = tempCallerId;
        }

        /// <summary>
        /// Получение информации о состоянии канала
        /// </summary>
        /// <param name="context">Строка с именем контекста. Обязателен.</param>
        /// <param name="exten">Строка с именем абонента. Обязателен.</param>
        /// <param name="actionid">Строка с идентификатором  запроса. Позволяет сопоставить результат выполнения конкретному запросу. Не обязателен.</param>
        /// <param name="result">Статус канала</param>
        public void GetHint([MarshalAs(UnmanagedType.BStr)] String context, [MarshalAs(UnmanagedType.BStr)] String exten, [MarshalAs(UnmanagedType.SysInt)] ref Int32 result)
        {
            if (!isConnected)
                throw new COMException("You are not connected.");

            if (isMonitoring)
            {
                isMonitoring = false;                
                pauseMonitor = true;
            }
            
            stopReceiveThread = true;
            Thread.Sleep(1000);
            StringBuilder cmdBuilder = new StringBuilder();
            cmdBuilder.Append("Action: ExtensionState\r\n");
            cmdBuilder.AppendFormat("Context: {0}\r\n", context);
            cmdBuilder.AppendFormat("Exten: {0}\r\n", exten);
            cmdBuilder.AppendFormat("ActionID: 12354657853324\r\n");
            cmdBuilder.Append("\r\n");

            CommandResult cmdResult = CommandDispatcher.Exec(cmdBuilder.ToString());

            StringBuilder sb = new StringBuilder();

            foreach (string elem in cmdResult.ResultData)
            {
                sb.Append(elem);
                if (Regex.Match(elem, "Status: ", RegexOptions.IgnoreCase).Success)
                {
                    if (!int.TryParse(elem.Split(':')[1].Trim(), out result))
                        result = -1000;
                    //EventGenerator.GenerateExtEvent(@"GetHint", sb.ToString());
                    return;
                }
            }

            //EventGenerator.GenerateExtEvent(@"GetHint", sb.ToString());
            result = -1001;

            if (pauseMonitor)
            {
                pauseMonitor = false;
                stopReceiveThread = false;
                
                Ready(monitoringChannel);                
            }
        }

        /// <summary>
        /// Опрос сокета (мониторинг)
        /// </summary>
        private void StartListenForReceive()
        {
            /*Получение и обработка событий входящего звонка от сервера
             * Описание пакета:
             * 
             * Event: Newstate
             * Privilege: call,all
             * Channel: <this_chanel>
             * ChannelState: 5
             * ChannelStateDesc: Ringing
             * CallerIDNum: <caller_id_num>
             * CallerIDName: <caller_id>
             * Uniqueid: <id>
             */
            
            isMonitoring = true;
            string response = "";

            //Бесконечный цикл, который опрашивает сокет
            while (!stopReceiveThread)
            {
                Thread.Sleep(1000);
                int bytesRead = 0;
                byte[] buffer;
                do
                {
                    buffer = new byte[1024];
                    //buffer.Initialize();
                    try
                    {
                        bytesRead = ApplicationManager.Instance.ClientSocket.Receive(buffer);
                        response = Encoding.ASCII.GetString(buffer, 0, bytesRead);                      
                        foreach (string elem in TextHelper.GetStringList(response, "\r\n\r\n"))
                        {
                            if (Regex.Match(elem, "Event: Newstate", RegexOptions.IgnoreCase).Success &&
                                Regex.Match(elem, string.Format("Channel: {0}", monitoringChannel), RegexOptions.IgnoreCase).Success &&
                                Regex.Match(elem, "ChannelStateDesc: Ringing", RegexOptions.IgnoreCase).Success &&
                                !Regex.Match(elem, string.Format("ConnectedLineNum: {0}", monitoringChannel.Split('/')[1].Trim().Substring(0, 4)), RegexOptions.IgnoreCase).Success)
                            {
                                foreach (string elem2 in TextHelper.GetStringList(response, "\r\n"))
                                {
                                    if (Regex.Match(elem2, "ConnectedLineNum: ", RegexOptions.IgnoreCase).Success)//ConnectedLineName:
                                    {
                                        tempCallerId = elem2.Split(':')[1].Trim(); 
                                        EventGenerator.GenerateExtEvent(@"Ringing", string.Format("CallerID: {0}", tempCallerId));
                                        bytesRead = 0;
                                        break;
                                    }
                                }
                            }
                        }                            
                    }
                    catch (SocketException socex)
                    {
                        break;
                        //throw new COMException(socex.Message);
                    }                    
                } while (bytesRead > 0);
            }
            isMonitoring = false;
        }
    }
}
